/*
 * RangedNumber.java
 *
 * Created on August 3, 2001, 5:41 PM
 *
 * Copyright 2008 the Desert Labs Project (http://desertlabs.us)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package us.desertlabs.number;

import java.math.BigDecimal;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * This class is used to represent any <CODE>Number</CODE> that can fall within a range of
 * values. It stores not only the minimum and maximum values of the range, but
 * also a single actual value that falls somewhere inside that range.
 * In game development, this class is perfect for representing things like
 * character attributes, ammunition levels on weapons, or current speed of a
 * vehicle.
 *
 * @author Brandon Franklin
 */
public class RangedNumber extends Number implements Comparable<Number>, Cloneable {

    static final private long serialVersionUID = 4049636802178854960L;

    /** The <CODE>NumberRange</CODE> within which this value must fall */
    final private NumberRange range;

    /** The actual value represented by this object */
    final private Number value;

    /**
     * Creates a new <CODE>RangedNumber</CODE>. The actual value will be set
     * to the default, which is 0, and the minimum and maximum values of the
     * range will be set to 0 and 100, respectively.
     */
    public RangedNumber() {
        this( new NumberRange( 0, 100 ) );
    }

    /**
     * Creates a new <CODE>RangedNumber</CODE> with the specified range of
     * legal values. The actual value will be set to the minimum legal value.
     *
     * @param range the range of allowed values for the number
     */
    public RangedNumber( final NumberRange range ) {
        this( range, range.getMinimum() );
    }

    /**
     * Creates a new <CODE>RangedNumber</CODE> with the specified range of
     * legal values. The actual value will be set to the specified value.
     *
     * @param range the range of allowed values for the number
     * @param value the actual value
     * @throws IllegalArgumentException if the supplied current value does not
     *         fall within the supplied range
     */
    public RangedNumber( final NumberRange range, final Number value ) {
        this.range = range;
        if( range.includes( value ) ) {
            this.value = value;
        } else {
            throw new IllegalArgumentException( "Specified value does not fall within the specified range." );
        }
    }

    /**
     * Builds and returns a clone of this object.
     *
     * @return a clone of this object
     */
    @Override
    public Object clone() {
        RangedNumber clone = null;
        try {
            clone = (RangedNumber)super.clone();
        } catch( final CloneNotSupportedException cnse ) {
            Logger.getLogger( getClass().getName() ).log( Level.SEVERE, "Unable to clone RangedNumber object.", cnse );
        }

        return clone;
    }

    /**
     * Compares this <CODE>RangedNumber</CODE> with the specified <CODE>Number</CODE>.
     * The actual values of the two numbers will be compared.
     * <P>
     * Notably, this means this comparison method is <I>inconsistent with equals</I>
     * and therefore caution should be used when working with <CODE>SortedSet</CODE>s
     * or <CODE>SortedMap</CODE>s.
     *
     * @param obj <CODE>Number</CODE> to which this <CODE>RangedNumber</CODE>
     *        is to be compared
     * @return a negative number, zero, or a positive number as this <CODE>RangedNumber</CODE>'s
     *         current value is numerically less than, equal to, or greater than
     *         <CODE>obj</CODE>
     * @see Comparable
     */
    @Override
    public int compareTo( final Number obj ) {
        final BigDecimal objDec = new BigDecimal( obj.toString() );
        final BigDecimal valueDec = new BigDecimal( value.toString() );
        return valueDec.compareTo( objDec );
    }

    /**
     * Decreases the value represented by this object by the supplied amount and
     * returns the resulting new <CODE>Number</CODE>.
     *
     * @param decrease the amount by which to decrease this object's value
     * @return a new <CODE>RangedNumber</CODE> instance representing the result
     *         of the decrease in value
     * @throws IllegalArgumentException if the specified decrease would cause
     *         the object's value to lie outside the allowed range
     */
    public RangedNumber decreaseValueBy( final Number decrease ) {
        BigDecimal valDec = new BigDecimal( getValue().toString() );
        final BigDecimal decDec = new BigDecimal( decrease.toString() );

        valDec = valDec.subtract( decDec );
        try {
            return new RangedNumber( range, decDec );
        } catch( final IllegalArgumentException e ) {
            throw new IllegalArgumentException( "Specified decrease would move value outside of the allowed range." );
        }
    }

    /**
     * Returns the value represented by this object as a <CODE>double</CODE>.
     *
     * @return the value represented by this object as a <CODE>double</CODE>
     */
    @Override
    public double doubleValue() {
        return value.doubleValue();
    }

    /**
     * Compares this <CODE>RangedNumber</CODE> with the specified <CODE>Object</CODE>
     * for equality.
     *
     * @param obj <CODE>Object</CODE> to which this <CODE>RangedNumber</CODE>
     *        is to be compared
     * @return <CODE>true</CODE> if and only if the specified <CODE>Object</CODE>
     *         is a <CODE>RangedNumber</CODE> whose value is numerically equal
     *         to this <CODE>RangedNumber</CODE>'s value, and whose <CODE>NumberRange</CODE>
     *         is also equal to this one's
     */
    @Override
    public boolean equals( final Object obj ) {
        if( obj == this ) {
            return true;

        } else if( obj instanceof RangedNumber ) {
            final RangedNumber otherNum = (RangedNumber)obj;

            // Check the values
            if( !otherNum.value.equals( value ) ) {
                return false;

            } else // Check the ranges
            if( !otherNum.range.equals( range ) ) {
                return false;

            } else {
                return true;
            }

        } else {
            return false;
        }
    }

    /**
     * Returns the value represented by this object as a <CODE>float</CODE>.
     *
     * @return the value represented by this object as a <CODE>float</CODE>
     */
    @Override
    public float floatValue() {
        return value.floatValue();
    }

    /**
     * Returns the <CODE>NumberRange</CODE> that constrains this <CODE>RangedNumber</CODE>.
     *
     * @return the <CODE>NumberRange</CODE> that constrains this <CODE>RangedNumber</CODE>
     */
    public NumberRange getRange() {
        return range;
    }

    /**
     * Returns the value represented by this object.
     *
     * @return the value represented by this object
     */
    public Number getValue() {
        return value;
    }

    /**
     * Returns the hash value for this <CODE>RangedNumber</CODE>, which will
     * be generated based on its numerical value and its range, using the
     * algorithm supplied by the various <CODE>Number</CODE> objects'
     * implementations.
     *
     * @return the hash value for this <CODE>RangedNumber</CODE>
     */
    @Override
    public int hashCode() {
        return (value.hashCode() + range.hashCode());
    }

    /**
     * Increases the value represented by this object by the supplied amount and
     * returns the resulting new <CODE>Number</CODE>.
     *
     * @param increase the amount by which to increase this object's value
     * @return a new <CODE>RangedNumber</CODE> instance representing the result
     *         of the increase in value
     * @throws IllegalArgumentException if the specified increase would cause
     *         the object's value to lie outside the allowed range
     */
    public RangedNumber increaseValueBy( final Number increase ) {
        BigDecimal valDec = new BigDecimal( value.toString() );
        final BigDecimal incDec = new BigDecimal( increase.toString() );

        valDec = valDec.add( incDec );
        try {
            return new RangedNumber( range, valDec );
        } catch( final IllegalArgumentException e ) {
            throw new IllegalArgumentException( "Specified increase would move value outside of the allowed range." );
        }
    }

    /**
     * Returns the value represented by this object as an <CODE>int</CODE>.
     *
     * @return the value represented by this object as an <code>int</CODE>
     */
    @Override
    public int intValue() {
        return value.intValue();
    }

    /**
     * Returns the value represented by this object as a <CODE>long</CODE>.
     *
     * @return the value represented by this object as a <CODE>long</CODE>
     */
    @Override
    public long longValue() {
        return value.longValue();
    }

    /**
     * Returns a <CODE>String</CODE> representation of this number.
     *
     * @return a <CODE>String</CODE> representation of this number
     */
    @Override
    public String toString() {
        return value.toString();
    }
}
